#!/bin/bash
# Port forward script
# acetone at mail.i2p
# 2022

# CONSTANTS
INPUT_INTERFACE="changeme"
OUTPUT_INTERFACE="changeme"
SETTINGS_STORAGE="/srv/portforward.txt"

######################
######################
# LOGIC

RULES=() # inport,dest_addr,dest_port

usage() {
    echo "PORT FORWARD SCRIPT (v0.0.1) USAGE    ◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤"
    echo "--command--arguments------------------description----------"
    echo ""
    echo "* add      <inport> <address> <port>  Add rule to forward"
    echo "  ﹂example:"
    echo "           add 80 192.168.1.3 8080"
    echo "               ^  ^           ^ destination port"
    echo "               |  └ destination address"
    echo "               └ inport"
    echo ""
    echo "* del      <address> <port>           Delete existed rule"
    echo "  ﹂example:"
    echo "           del 80 192.168.1.3 8080"
    echo "               ^  ^           ^ destination port"
    echo "               |  └ destination address"
    echo "               └ inport"
    echo ""
    echo "* list                                 Display existed rules"
    echo ""
    echo "* init                                 Apply rules at start"
    echo ""
    echo "* install                              Install at first time"
    echo ""
    echo "◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤◢◤"
}

message_about_interfaces() {
    echo "Please edit first lines of script to define interfaces correctly"
    echo "Script path: nano /usr/sbin/port-forward.sh"
}

check_interface() {
    if [[ $(ip a | grep $INPUT_INTERFACE) == "" ]]; then
        echo "Input interface $INPUT_INTERFACE not exists"
        message_about_interfaces
        exit
    fi
    if [[ $(ip a | grep $OUTPUT_INTERFACE) == "" ]]; then
        echo "Output interface $OUTPUT_INTERFACE not exists"
        message_about_interfaces
        exit
    fi
}

check_to_root() {
    if [ "$EUID" -ne 0 ]; then
        echo "Please run as root"
        exit 1
    fi
}

in_port() {
    echo $1 | cut -f1 -d,
}

dest_address() {
    echo $1 | cut -f2 -d,
}

dest_port() {
    echo $1 | cut -f3 -d,
}

enable_forwarding() {
    check_to_root

    sed -i 's/\#net.ipv4.ip_forward=1/net.ipv4.ip_forward=1/' /etc/sysctl.conf
    sysctl net.ipv4.ip_forward=1
    sysctl -p

    systemctl stop nftables
    systemctl disable nftables

    apt update
    apt install iptables -y
}

add() {
    INPORT=$1
    DEST_ADDRESS=$2
    DEST_PORT=$3

    if [[ $(iptables -t nat -L -n | grep "dpt:$INPORT ") != "" ]]; then
        echo "Inport $INPORT already in use. Request rejected."
        echo "Try command 'list' to see existed rules and 'del' to remove some one."
        exit 1
    fi

    iptables -A PREROUTING -t nat -i $INPUT_INTERFACE -p tcp --dport $INPORT -j DNAT --to $DEST_ADDRESS:$DEST_PORT
    if [[ $? != 0 ]]; then
        echo "Incorrect input data"
        exit 1
    fi
    iptables -A PREROUTING -t nat -i $INPUT_INTERFACE -p udp --dport $INPORT -j DNAT --to $DEST_ADDRESS:$DEST_PORT

    echo "$INPORT,$DEST_ADDRESS,$DEST_PORT" >> $SETTINGS_STORAGE

    if [[ $(iptables -t nat -L -v | grep -e "^.*MASQUERADE.*$OUTPUT_INTERFACE.*$") == "" ]]; then
        iptables -t nat -A POSTROUTING -o $OUTPUT_INTERFACE -j MASQUERADE
    fi
}

del() {
    INPORT=$1
    DEST_ADDRESS=$2
    DEST_PORT=$3

    exists=false

    for rule in "${RULES[@]}"
    do
        if [[ $INPORT == $(in_port $rule) ]]; then
            if [[ $DEST_ADDRESS == $(dest_address $rule) ]]; then
                if [[ $DEST_PORT == $(dest_port $rule) ]]; then
                    exists=true
                    break
                fi
            fi
        fi
    done

    if [[ $exists != true ]]; then
        echo "Rule not exists"
        exit 1
    fi

    iptables -D PREROUTING -t nat -i $INPUT_INTERFACE -p tcp --dport $INPORT -j DNAT --to $DEST_ADDRESS:$DEST_PORT
    iptables -D PREROUTING -t nat -i $INPUT_INTERFACE -p udp --dport $INPORT -j DNAT --to $DEST_ADDRESS:$DEST_PORT

    sed -i "/^${INPORT},${DEST_ADDRESS},${DEST_PORT}$/d" $SETTINGS_STORAGE
}

init() {
    if [ -f "$SETTINGS_STORAGE" ]; then

        while read -r line; do  
            if [[ $(in_port $line) == "" ]]; then
                continue
            fi
            if [[ $(dest_address $line) == "" ]]; then
                continue
            fi
            if [[ $(dest_port $line) == "" ]]; then
                continue
            fi

            RULES+=("$line")
        done < $SETTINGS_STORAGE

        for rule in "${RULES[@]}"
        do
            if [[ $(iptables -t nat -L -n | grep "dpt:$(in_port $rule) to:$(dest_address $rule):$(dest_port $rule)") == "" ]]; then
                 echo $(in_port $rule) $(dest_address $rule) $(dest_port $rule) > /dev/null
            fi
        done

    fi

    check_interface
}

install() {
    if [[ $(crontab -l | grep "@reboot /usr/sbin/port-forward.sh init") != "" ]]; then
        if [[ $2 != "forced" ]]; then
            echo "Seems like already installed!"
            echo "To force the installation run this script as 'install forced'"
            exit 1
        fi
    fi

    systemctl enable cron

    cp $1 /usr/sbin/port-forward.sh
    if [[ $? != 0 ]]; then
        echo "Copy script to /usr/sbin/port-forward.sh failed"
        exit 1
    fi

    echo -e "@reboot /usr/sbin/port-forward.sh init" > /tmp/cronrule.tmp
    crontab /tmp/cronrule.tmp
    rm /tmp/cronrule.tmp

    enable_forwarding

    rm $1
    clear
    echo "Installed"
    echo "Using via 'port-forward.sh'"
    echo "Edit via 'nano /usr/sbin/port-forward.sh'"
}

if [[ $1 == "init" ]]; then
    check_to_root
    init
    exit 0
fi

if [[ $1 == "install" ]]; then
    check_to_root
    install $0 $2
    exit 0
fi

if [[ $1 == "add" ]]; then
    if [[ $2 == "" ]]; then
        echo "Expected inport after 'add' command"
        exit 1
    fi

    if [[ $3 == "" ]]; then
        echo "Expected destination address after 'add <inport>' command"
        exit 1
    fi

    if [[ $4 == "" ]]; then
        echo "Expected destination port after 'add <inport> <address>' command"
        exit 1
    fi

    check_to_root
    init
    add $2 $3 $4
    exit 0
fi

if [[ $1 == "del" ]]; then
    if [[ $2 == "" ]]; then
        echo "Expected inport after 'del' command"
        exit 1
    fi

    if [[ $3 == "" ]]; then
        echo "Expected destination address after 'del <inport>' command"
        exit 1
    fi

    if [[ $4 == "" ]]; then
        echo "Expected destination port after 'del <inport> <address>' command"
        exit 1
    fi

    check_to_root
    init
    del $2 $3 $4
    exit 0
fi

if [[ $1 == "list" ]]; then
    check_to_root
    init
    echo "Input interface: $INPUT_INTERFACE"
    echo "Output interface: $OUTPUT_INTERFACE"
    echo ""
    for rule in "${RULES[@]}"
    do
        echo -e "[$(in_port $rule)] -> $(dest_address $rule):$(dest_port $rule)"
        echo -e "Remove command: del $(in_port $rule) $(dest_address $rule) $(dest_port $rule)"
        echo ""
    done
    exit 0
fi

usage
